package HDAPI;

/*
** Copyright (c) 2009
 * 
 * Richard Uren <richard@teleport.com.au>
 * Faisal Thaheem <faisal@computedsynergy.com>
 * 
** All Rights Reserved
**
** --
**
** LICENSE: Redistribution and use in source and binary forms, with or
** without modification, are permitted provided that the following
** conditions are met: Redistributions of source code must retain the
** above copyright notice, this list of conditions and the following
** disclaimer. Redistributions in binary form must reproduce the above
** copyright notice, this list of conditions and the following disclaimer
** in the documentation and/or other materials provided with the
** distribution.
**
** THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
** MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
** NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
** BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
** OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
** TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
** USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
** DAMAGE.
**
** --
**
** This is a reference implementation for interfacing with www.handsetdetection.com
**
*/

import java.awt.Rectangle;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Set;

import org.apache.commons.codec.binary.Base64;
import org.json.JSONException;
import org.json.JSONObject;

import HDAPI.utilities.WSSEUtilities;

public class HDBase {
    private final static String DETECT_SERVICE = "/devices/detect.json";
    private final static String DETECT_ALL_SERVICE = "/devices/detectall.json";
    private final static String INFORMATION_SERVICE = "/devices/information.json";
    private final static String MODELS_SERVICE = "/devices/models.json";
    private final static String VENDORS_SERVICE = "/devices/vendors.json";
    private final static String DOWNLOAD_SERVICE = "/devices/download.json";
	
    //Where we will post our queries 
    private String m_url = Settings.getURL();
    
    //Site id
    private String m_siteId = Settings.getSiteId();
    
    //Our API Key
    private String m_apiKey = Settings.getApiKey();//"8a4a663ba61f9ac9407894a370871bf3";
    
    //Secret
    private String m_secret = Settings.getSecret();
    
    //Username
    private String m_username = Settings.getUsername();
    
    //Connect timeout in seconds
    private int m_connectTimeout = Settings.getConnectTimeout();
    
    //Read timeout in seconds
    private int m_readTimeout = Settings.getReadTimeout();
    
    //Parameters to send for detection request
    private JSONObject m_detectRequest = createRequest();
    
    //Response of detection
    private JSONObject m_detectReply = new JSONObject();
    
    //Vendor reply
    private JSONObject m_vendorReply = new JSONObject();
    
    //Model reply
    private JSONObject m_modelReply = new JSONObject();
    
    private JSONObject m_informationReply = new JSONObject();
    
    private File m_downloadFile = null; 
    
    //Error string
    private String m_error = "";
    
    private int m_lastURLIndex = -1;
    
    /**
     * Default constructor.
     */
    public HDBase() {
    	
    }
    
    /**
     * Legacy constructor for back compatibility. username and secret can be set in the hdapi_config.properties file.
     * @deprecated
     * @param username Username for API authentication.
     * @param secret Secret for API authentication.
     */
    public HDBase(String username, String secret){
        this.m_username = username;
        this.m_secret = secret;
    }
    
    /**
     * Setter method for HDBase error message. 
     * @param msg Message string to set.
     */
	public void setError(String msg) {
		this.m_error = msg;
	}
	
	/**
	 * Setter method for site id.
	 * @param id Site id to set.
	 */
	public void setSiteId(String id) {
		this.m_siteId = id;
	}

	/**
	 * Setter method for URL of API server.
	 * @param urls Array of URLs
	 */
	public void setURL(String url) {
		this.m_url = url;
	}

	/**
	 * Setter method of v2ApiKey.
	 * @param apiKey API Key to set.
	 */
	public void setv2ApiKey(String apiKey) {
		this.m_apiKey = apiKey;
	}

	/**
	 * Setter method for secret.
	 * @param secret
	 */
	public void setSecret(String secret) {
		this.m_secret = secret;
	}

	/**
	 * Setter method for API username.
	 * @param username
	 */
	public void setUsername(String username) {
		this.m_username = username;
	}

	/**
	 * Setter method for socket connect timeout in seconds.
	 * @param timeout
	 */
	public void setConnectTimeout(int timeout) {
		this.m_connectTimeout = timeout;
	}

	/**
	 * Setter method for socket read timeout in seconds.
	 * @param timeout
	 */
	public void setReadTimeout(int timeout) {
		this.m_readTimeout = timeout;
	}

	/**
	 * Setter method for detect variable.
	 * @param key Variable key, e.g. user-agent
	 * @param val Variable value
	 */
	public void setDetectVar(String key, String val) {
		try {
			this.m_detectRequest.put(key, val);
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			setError(ex.getMessage());
		}
	}
    
    /**
     * Getter method of last set error message.
     * @return
     */
	public String getError() {
		return this.m_error;
	}

	/**
	 * Getter method for site id. Default value is loaded from hdapi_config.properties - common.site_id
	 * @return site id
	 */
	public String getSiteId() {
		return this.m_siteId;
	}

	/**
	 * Getter method for URLs of API server. Default Value is loaded from hdapi_config.properties - common.urls
	 * @return
	 */
	public String getURL() {
		return this.m_url;
	}

	/**
	 * Getter method of v2ApiKey. Default value is loaded from hdapi_config.properties - common.api_key
	 * @return V2 API Key
	 */
	public String getv2ApiKey() {
		return this.m_apiKey;
	}

	/**
	 * Getter method of API secret. Default value is loaded from hdapi_config.properties - common.api_key
	 * @return
	 */
	public String getSecret() {
		return this.m_secret;
	}

	/**
	 * Getter method of API username. Default value is loaded from hdapi_config.properties - common.username
	 * @return
	 */
	public String getUsername() {
		return this.m_username;
	}

	/**
	 * Getter method of socket connect timeout value in seconds. Default value is loaded from hdapi_config.properties - connection.connect_timeout
	 * @return
	 */
	public int getConnectTimeout() {
		return this.m_connectTimeout;
	}

	/**
	 * Getter method of socket read timeout value in seconds. Default value is loaded from hdapi_config.properties - connection.read_timeout
	 * @return
	 */
	public int getReadTimeout() {
		return this.m_readTimeout;
	}

	/**
	 * Getter method of Detect/DetectAll request object.
	 * @return
	 */
	public JSONObject getDetectRequest() {
		return this.m_detectRequest;
	}

	/**
	 * Getter method of Detect/DetectAll reply object.
	 * @return
	 */
	public JSONObject getDetect() {
		return this.m_detectReply;
	}

	/**
	 * Getter method of Vendor reply object.
	 * @return
	 */
	public JSONObject getVendor() {
		return this.m_vendorReply;
	}

	/**
	 * Getter method of Model reply object.
	 * @return
	 */
	public JSONObject getModel() {
		return this.m_modelReply;
	}

	/**
	 * Getter method of Information reply object.
	 * @return
	 */
	public JSONObject getInformation() {
		return this.m_informationReply;
	}

	/**
	 * Get the downloaded file location of Download method.
	 * @return
	 */
	public File getDownloadFile() {
		return this.m_downloadFile;
	}

    private boolean post(String data, String service, HashMap optional_headers, OutputStream response) 
    	throws Exception 
    {
    	boolean ret = false;
    	
    	URL oldURL = new URL(m_url);
    	InetAddress[] addresses = resolveHostAddress(oldURL);
    	ArrayList<Integer> randomIndexes = createRandomIndexArray(addresses.length);
    	for (int i = 0; i < addresses.length ; i++) {
	        try{
	        	InetAddress address = addresses[randomIndexes.get(i)];
	        	URL newURL = new URL(m_url.replace(oldURL.getHost(), address.getHostAddress()) + service);
		        //URL url = new URL(strURL + service);
		        
		        URLConnection conn;
		        if (Settings.isUseProxy() && !"".equals(Settings.getProxyServer())) {
		        	Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(Settings.getProxyServer(), Settings.getProxyPort()));
		        	conn = newURL.openConnection(proxy);
		        	if (!"".equals(Settings.getUsername())) {
		        		conn.setRequestProperty("Proxy-Authorization", getBasicProxyPass());
		        	}
		        } else {
		        	conn = newURL.openConnection();
		        }
		        conn.setConnectTimeout(getConnectTimeout() * 1000);
		        //= url.openConnection();
		        conn.setDoOutput(true);
		        conn.setRequestProperty("Content-Type", "application/json");
		        conn.setRequestProperty("ApiKey",this.m_apiKey);
		        //conn.setRequestProperty("Authorization",(String)optional_headers.get("Authorization"));
		        conn.setRequestProperty("X-wsse",(String)optional_headers.get("X-WSSE"));
		        conn.setReadTimeout(getReadTimeout() * 1000);
		        
		        OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
		        writer.write(data);
		        writer.flush();
		        InputStream is = conn.getInputStream();
		        byte b = -1;
		        while((b = (byte)is.read()) != -1){
		        	response.write(b);
		        }
		        response.flush();
		        is.close();
		        response.close();
		        setError("");
		        return ret = true;
	        }catch(Exception ex){
	        	setError("Service not found : " + ex.getMessage());
	        }
        }
    	return ret;
    }

    private JSONObject getJSONResponse(JSONObject data, String service)
    	throws Exception
    {
        
        JSONObject reply = null;
        
        String wsse = this.generateWSSEHeader(this.m_username, this.m_secret);
        HashMap<String, String> optionalHeaders = new HashMap<String,String>();
        optionalHeaders.put("X-WSSE", wsse);
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        if (post(data==null? "": data.toString(), service, optionalHeaders, bytes)) {
        	String response = new String(bytes.toByteArray(), "UTF-8");
        	reply = new JSONObject(response);
        }
        
        return reply;
    }
    
    private File saveResponseAsFile(JSONObject data, String service)
    	throws Exception
    {
       	File ret = null;
    
        String wsse = this.generateWSSEHeader(this.m_username, this.m_secret);
        HashMap<String, String> optionalHeaders = new HashMap<String,String>();
        optionalHeaders.put("X-WSSE", wsse);
        ret = File.createTempFile("hddownload", ".json");
        FileOutputStream fos = new FileOutputStream(ret);
        if (!post(data==null? "": data.toString(), service, optionalHeaders, fos)) {
        	ret = null;
        }
        
        
        return ret;
    }
    
    private InetAddress[] resolveHostAddress(URL url) throws Exception {
    	String host = url.getHost();
    	InetAddress[] ret = InetAddress.getAllByName(host);
    	return ret;
    }
    
    private JSONObject createRequest() {
    	JSONObject request = new JSONObject();
    	try {
    		if (getSiteId() != null && !"".equals(getSiteId()))
    			request.put("site", this.getSiteId());
    	} catch (Exception ex) {
    		System.out.println(ex.getMessage());
    		setError(ex.getMessage());
    	}
    	return request;
    }
    /**
     * Calling this function prior to calling detect() fills in the necessary 
     * information for a lookup
     * @param eHeaders Enumeration of headers in key value pairs
     * @param consumer a class that implements HDAPIConsumer interface
     */
    public void detectInit(HashMap<String, String> headers, String serverIpAddress, String requestURI)
    {
    	Set<String> keys = headers.keySet();
        for (String key : keys) {
            if(key.matches("/(^x|^X|^http|^HTTP|^Http)/")){
                try{
                    this.m_detectRequest.put(key, headers.get(key));
                    System.out.println(key + " " + headers.get(key));
                }catch(Exception ex){
                    System.out.println(ex.getMessage());
                    setError(ex.getMessage());
                }
            }
        }
        
        try{
        	
            this.m_detectRequest.put("user-agent",headers.get("user-agent"));
            this.m_detectRequest.put("ipaddress", serverIpAddress);
            this.m_detectRequest.put("request_uri", requestURI);
            
            
        }catch(Exception ex){
            System.out.println(ex.getMessage());
            setError(ex.getMessage());
        }
    }
    
    /**
     * Queries handsetdetection.com for device capabilities
     * @param options all,display,geoip,product_info or handsetdetection.com 
     * for complete reference
     * @return true if successful and false otherwise
     */
    public boolean detect(String options){

        boolean ret = false;
        
        try{
            this.m_detectRequest.put("options", options);
            this.m_detectReply = getJSONResponse(this.m_detectRequest, DETECT_SERVICE);
            
            if(this.m_detectReply.has("message") && this.m_detectReply.getString("message").equalsIgnoreCase("OK")){
                ret = true;
            }
            
        }catch(Exception ex){
        	setError("Service not found : " + m_url + DETECT_SERVICE);
        	m_detectReply = new JSONObject();
        	createErrorReply(m_detectReply);        }
        
        
        return ret;

    }
    
    /**
     * Queries handsetdetection.com for all device capabilities
     * @return true if successful and false otherwise
     */
    public boolean detectAll(){

        boolean ret = false;
        
        try{
            this.m_detectReply = getJSONResponse(this.m_detectRequest, DETECT_ALL_SERVICE);
            
            if(this.m_detectReply.has("message") && this.m_detectReply.getString("message").equalsIgnoreCase("OK")){
                ret = true;
            }
            
        }catch(Exception ex){
            setError("Service not found : " + m_url + DETECT_ALL_SERVICE);
        	m_detectReply = new JSONObject();
        	createErrorReply(m_detectReply);        
        }
        
        
        return ret;

    }
    
    /**
     * Fetches all supported Vendors available at handsetdetection.com
     * @return true on success and false otherwise
     */
    public boolean vendor(){
        
        boolean ret = false;
        
        try{
            this.m_vendorReply = getJSONResponse(createRequest(), VENDORS_SERVICE);
            
            if(this.m_vendorReply.has("message") && this.m_vendorReply.getString("message").equalsIgnoreCase("OK")){
                ret = true;
            }
            
        }catch(Exception ex){
        	setError("Service not found : " + m_url + VENDORS_SERVICE);
        	m_vendorReply = new JSONObject();
        	createErrorReply(m_vendorReply);        
        }
        
        return ret;
        
    }

    /**
     * Fetches all available phone models in handsetdetection.com database. 
     * If a vendor is specified then only models for that vendor are returned. 
     * Call getModel() to get access to the returned list.
     * @param vendor all or a valid vendor name
     * @return true if successful, false otherwise
     */
    public boolean model(String vendor){
        
        boolean ret = false;
        
        try{
            JSONObject modelRequest = createRequest();
            modelRequest.put("vendor", vendor);
            this.m_modelReply = getJSONResponse(modelRequest, MODELS_SERVICE);
            
            if(this.m_modelReply.has("message") && this.m_modelReply.getString("message").equalsIgnoreCase("OK")){
                ret = true;
            }
            
        }catch(Exception ex){
        	setError("Service not found : " + m_url + MODELS_SERVICE);
        	m_modelReply = new JSONObject();
        	createErrorReply(m_modelReply);
        }
        
        return ret;
        
    }
    
    /**
     * Fetches device information of a specific model created by  specific vendor.
     * @param vendor Vendor name
     * @param model Model name
     * @return
     */
    public boolean information(String vendor, String model){
        
        boolean ret = false;
        
        try{
            JSONObject informationRequest = createRequest();
            informationRequest.put("vendor", vendor);
            informationRequest.put("model", model);
            this.m_informationReply = getJSONResponse(informationRequest, INFORMATION_SERVICE);
            
            if(this.m_informationReply.has("message") && this.m_informationReply.getString("message").equalsIgnoreCase("OK")){
                ret = true;
            }
            
        }catch(Exception ex){
            setError("Service not found : " + m_url + INFORMATION_SERVICE);
            m_informationReply = new JSONObject();
        	createErrorReply(m_informationReply);
        }
        
        
        return ret;
        
    }
    
    /**
     * Download all the devices information as a json response and save to a temporary file. Path of the temporary file can be found by method getDownloadedFile.
     * @return True is method succeed.
     */
    public boolean download(){
        
        boolean ret = false;
        
        try{
            JSONObject downloadRequest = createRequest();
            this.m_downloadFile = saveResponseAsFile(downloadRequest, DOWNLOAD_SERVICE);
            ret = (this.m_downloadFile != null);
        }catch(Exception ex){
        	setError("Service not found : " + m_url + DOWNLOAD_SERVICE);
        }
        
        
        return ret;
        
    }
    
    /**
     * Utility method for querying device phone call string. detect() function 
     * must have been called prior to calling this function
     * @return empty string if not supported or on error. Valid call string on 
     * success
     */
    public String getDeviceClickToCall(){
        
        String ret = null;
        
        try{
            
            if(this.m_detectReply.has("xhtml_ui")){
                ret = this.m_detectReply.getJSONObject("xhtml_ui").getString("xhtml_make_phone_call_string");
            }else{
                ret = "";
            }
            
        }catch(Exception ex){
            ret = null;
            System.out.println(ex.getMessage());
            setError(ex.getMessage());
        }
        
        return ret;
    }

    /**
     * Utility method for querying device screen resolution. detect() must have 
     * been called prior to calling this function
     * @return null or a valid Rectangle object
     */
    public java.awt.Rectangle getDeviceResolution(){
        
        Rectangle ret = null;
        
        try{
            
            if(this.m_detectReply.has("display")){

                ret = new Rectangle(
                    Integer.parseInt(this.m_detectReply.getJSONObject("display").getString("resolution_width")),
                    Integer.parseInt(this.m_detectReply.getJSONObject("display").getString("resolution_height"))
                );
                
            }else{
                ret = new Rectangle(0,0);
            }
            
        }catch(Exception ex){
            ret = null;
            System.out.println(ex.getMessage());
            setError(ex.getMessage());
        }
        
        return ret;
    }
    
    /**
     * Utility method for returning the SMS sending string of the device 
     * if supported, detect() must have been called prior to calling this 
     * function.
     * @return must have been called prior to calling this function.
     */
    public String getDeviceSendSms(){
        
        String ret = null;
        
        try{
            
            if(this.m_detectReply.has("xhtml_ui")){
                ret = this.m_detectReply.getJSONObject("xhtml_ui").getString("xhtml_send_sms_string");
            }else{
                ret = "";
            }
            
        }catch(Exception ex){
            ret = null;
            System.out.println(ex.getMessage());
            setError(ex.getMessage());
        }
        
        return ret;
    }
    
    /**
     * Generates the WSSE header for authentication
     * @param username
     * @param password
     * @return header authentication string
     * @throws java.lang.Exception
     */
    public String generateWSSEHeader(String username, String password)

    {  
    	String ret = "";
    	try {
	        byte[] nonceBytes = Long.toString(new Date().getTime()).getBytes();
	        String nonce = new String(WSSEUtilities.base64Encode(nonceBytes));
	        
	        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
	        String created = sdf.format(new Date());
	        
	        String digest;
			
				digest = WSSEUtilities.generateDigest(
				        nonceBytes, created.getBytes("UTF-8"), password.getBytes("UTF-8"));
			
	        
	        StringBuilder header = new StringBuilder("UsernameToken Username=\"");
	        header.append(username);
	        header.append("\", ");
	        header.append("PasswordDigest=\"");
	        header.append(digest);
	        header.append("\", ");
	        header.append("Nonce=\"");
	        header.append(nonce);
	        header.append("\", ");
	        header.append("Created=\"");
	        header.append(created);
	        header.append("\"");
	        ret = header.toString();
    	} catch (UnsupportedEncodingException ex) {
			System.out.println(ex.getMessage());
			setError(ex.getMessage());
		}
    	return ret;
    }
    
    /**
     * Check if the device to detect is mobile. Can only be called after detect/detectAll.
     * @return 
     */
    public boolean isMobile() 
    {
    	try {
			return m_detectReply.has("class") && "mobile".equalsIgnoreCase((m_detectReply.getString("class")));
		} catch (JSONException ex) {
			setError(ex.getMessage());
			return false;
		}
    }
    
    /**
     * Check if the device to detect is console. Can only be called after detect/detectAll.
     * @return
     */
    public boolean isConsole()
    {
    	try {
			return m_detectReply.has("class") && "console".equalsIgnoreCase((m_detectReply.getString("class")));
		} catch (JSONException ex) {
			setError(ex.getMessage());
			return false;
		}
    }
    
    /**
     * Check if the device to detect is tablet. Can only be called after detect/detectAll.
     * @return
     */
    public boolean isTablet()
    {
    	try {
			return m_detectReply.has("class") && "tablet".equalsIgnoreCase((m_detectReply.getString("class")));
		} catch (JSONException ex) {
			setError(ex.getMessage());
			return false;
		}
    }
    
    private String getBasicProxyPass()
    {
    	StringBuilder sb = new StringBuilder();
    	sb.append("Basic ");
    	sb.append(new String(Base64.encodeBase64((new String(Settings.getProxyUsername() + ":" + Settings.getProxyPassword())).getBytes())));
    	return sb.toString();

    }
    
    private void createErrorReply(JSONObject reply)
    {
    	try {
	    	reply.put("message", getError());
    	} catch(Exception e) {
    		setError("JSON error : " + e.getMessage());
    	}
    }
    
    private ArrayList<Integer> createRandomIndexArray(int length) {
    	ArrayList<Integer> orderedArray = new ArrayList<Integer>();
    	ArrayList<Integer> randomArray = new ArrayList<Integer>();
    	for (int i = 0 ; i < length; i++) {
    		orderedArray.add(i);
    	}
    	for (int i = 0 ; i < length; i ++) {
    		int index= (int) (Math.random() * 1000) % length;
    		while (randomArray.contains(index)) {
    			index = (int) (Math.random() * 1000) % length;
    		}
    		randomArray.add(index);
    	}
    	return randomArray;
    }

}
